/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.ide.ext.java.client.editor;


import java.util.List;

import javax.inject.Inject;

import org.eclipse.che.ide.api.editor.text.rules.Token;
import org.eclipse.che.ide.api.editor.text.rules.TokenImpl;
import org.eclipse.che.ide.ext.java.jdt.JavaPartitions;
import org.eclipse.che.ide.api.editor.partition.PartitionScanner;
import org.eclipse.che.ide.api.editor.partition.StringCharacterScanner;
import org.eclipse.che.ide.ext.java.jdt.text.rules.CharacterScanner;

/**
 * This scanner recognizes the JavaDoc comments, Java multi line comments, Java single line comments,
 * Java strings and Java characters.
 */
public class JavaPartitionScanner implements PartitionScanner, JavaPartitions {

    // states
    private static final int JAVA                = 0;
    private static final int SINGLE_LINE_COMMENT = 1;
    private static final int MULTI_LINE_COMMENT  = 2;
    private static final int JAVADOC             = 3;
    private static final int CHARACTER           = 4;
    private static final int STRING              = 5;

    // beginning of prefixes and postfixes
    private static final int NONE            = 0;
    private static final int BACKSLASH       = 1; // postfix for STRING and CHARACTER
    private static final int SLASH           = 2; // prefix for SINGLE_LINE or MULTI_LINE or JAVADOC
    private static final int SLASH_STAR      = 3; // prefix for MULTI_LINE_COMMENT or JAVADOC
    private static final int SLASH_STAR_STAR = 4; // prefix for MULTI_LINE_COMMENT or JAVADOC
    private static final int STAR            = 5; // postfix for MULTI_LINE_COMMENT or JAVADOC
    private static final int CARRIAGE_RETURN = 6; // postfix for STRING, CHARACTER and SINGLE_LINE_COMMENT

    /** The content. */
    private final StringCharacterScanner fScanner = new StringCharacterScanner();

    /** The offset of the last returned token. */
    private int fTokenOffset;
    /** The length of the last returned token. */
    private int fTokenLength;

    /** The state of the scanner. */
    private int fState;
    /** The last significant characters read. */
    private int fLast;
    /** The amount of characters already read on first call to nextToken(). */
    private int fPrefixLength;

    // emulate JavaPartitionScanner
    private boolean fEmulate = false;
    private int fJavaOffset;
    private int fJavaLength;

    private final Token[] fTokens = new Token[]{
            new TokenImpl(null),
            new TokenImpl(JAVA_SINGLE_LINE_COMMENT),
            new TokenImpl(JAVA_MULTI_LINE_COMMENT),
            new TokenImpl(JAVA_DOC),
            new TokenImpl(JAVA_CHARACTER),
            new TokenImpl(JAVA_STRING)
    };

    public JavaPartitionScanner(boolean emulate) {
        fEmulate = emulate;
    }

    @Inject
    public JavaPartitionScanner() {
        this(false);
    }

    /*
     * @see org.eclipse.jface.text.rules.ITokenScanner#nextToken()
     */
    public Token nextToken() {

        // emulate JavaPartitionScanner
        if (fEmulate) {
            if (fJavaOffset != -1 && fTokenOffset + fTokenLength != fJavaOffset + fJavaLength) {
                fTokenOffset += fTokenLength;
                return fTokens[JAVA];
            } else {
                fJavaOffset = -1;
                fJavaLength = 0;
            }
        }

        fTokenOffset += fTokenLength;
        fTokenLength = fPrefixLength;

        while (true) {
            final int ch = fScanner.read();

            // characters
            switch (ch) {
                case CharacterScanner.EOF:
                    if (fTokenLength > 0) {
                        fLast = NONE; // ignore last
                        return preFix(fState, JAVA, NONE, 0);

                    } else {
                        fLast = NONE;
                        fPrefixLength = 0;
                        return TokenImpl.EOF;
                    }

                case '\r':
                    // emulate JavaPartitionScanner
                    if (!fEmulate && fLast != CARRIAGE_RETURN) {
                        fLast = CARRIAGE_RETURN;
                        fTokenLength++;
                        continue;

                    } else {

                        switch (fState) {
                            case SINGLE_LINE_COMMENT:
                            case CHARACTER:
                            case STRING:
                                if (fTokenLength > 0) {
                                    final Token token = fTokens[fState];

                                    // emulate JavaPartitionScanner
                                    if (fEmulate) {
                                        fTokenLength++;
                                        fLast = NONE;
                                        fPrefixLength = 0;
                                    } else {
                                        fLast = CARRIAGE_RETURN;
                                        fPrefixLength = 1;
                                    }

                                    fState = JAVA;
                                    return token;

                                } else {
                                    consume();
                                    continue;
                                }

                            default:
                                consume();
                                continue;
                        }
                    }

                case '\n':
                    switch (fState) {
                        case SINGLE_LINE_COMMENT:
                        case CHARACTER:
                        case STRING:
                            // assert(fTokenLength > 0);
                            return postFix(fState);

                        default:
                            consume();
                            continue;
                    }

                default:
                    if (!fEmulate && fLast == CARRIAGE_RETURN) {
                        switch (fState) {
                            case SINGLE_LINE_COMMENT:
                            case CHARACTER:
                            case STRING:

                                int last;
                                int newState;
                                switch (ch) {
                                    case '/':
                                        last = SLASH;
                                        newState = JAVA;
                                        break;

                                    case '*':
                                        last = STAR;
                                        newState = JAVA;
                                        break;

                                    case '\'':
                                        last = NONE;
                                        newState = CHARACTER;
                                        break;

                                    case '"':
                                        last = NONE;
                                        newState = STRING;
                                        break;

                                    case '\r':
                                        last = CARRIAGE_RETURN;
                                        newState = JAVA;
                                        break;

                                    case '\\':
                                        last = BACKSLASH;
                                        newState = JAVA;
                                        break;

                                    default:
                                        last = NONE;
                                        newState = JAVA;
                                        break;
                                }

                                fLast = NONE; // ignore fLast
                                return preFix(fState, newState, last, 1);

                            default:
                                break;
                        }
                    }
            }

            // states
            switch (fState) {
                case JAVA:
                    switch (ch) {
                        case '/':
                            if (fLast == SLASH) {
                                if (fTokenLength - getLastLength(fLast) > 0) {
                                    return preFix(JAVA, SINGLE_LINE_COMMENT, NONE, 2);
                                } else {
                                    preFix(JAVA, SINGLE_LINE_COMMENT, NONE, 2);
                                    fTokenOffset += fTokenLength;
                                    fTokenLength = fPrefixLength;
                                    break;
                                }

                            } else {
                                fTokenLength++;
                                fLast = SLASH;
                                break;
                            }

                        case '*':
                            if (fLast == SLASH) {
                                if (fTokenLength - getLastLength(fLast) > 0) {
                                    return preFix(JAVA, MULTI_LINE_COMMENT, SLASH_STAR, 2);
                                } else {
                                    preFix(JAVA, MULTI_LINE_COMMENT, SLASH_STAR, 2);
                                    fTokenOffset += fTokenLength;
                                    fTokenLength = fPrefixLength;
                                    break;
                                }

                            } else {
                                consume();
                                break;
                            }

                        case '\'':
                            fLast = NONE; // ignore fLast
                            if (fTokenLength > 0) {
                                return preFix(JAVA, CHARACTER, NONE, 1);
                            } else {
                                preFix(JAVA, CHARACTER, NONE, 1);
                                fTokenOffset += fTokenLength;
                                fTokenLength = fPrefixLength;
                                break;
                            }

                        case '"':
                            fLast = NONE; // ignore fLast
                            if (fTokenLength > 0) {
                                return preFix(JAVA, STRING, NONE, 1);
                            } else {
                                preFix(JAVA, STRING, NONE, 1);
                                fTokenOffset += fTokenLength;
                                fTokenLength = fPrefixLength;
                                break;
                            }

                        default:
                            consume();
                            break;
                    }
                    break;

                case SINGLE_LINE_COMMENT:
                    consume();
                    break;

                case JAVADOC:
                    switch (ch) {
                        case '/':
                            switch (fLast) {
                                case SLASH_STAR_STAR:
                                    return postFix(MULTI_LINE_COMMENT);

                                case STAR:
                                    return postFix(JAVADOC);

                                default:
                                    consume();
                                    break;
                            }
                            break;

                        case '*':
                            fTokenLength++;
                            fLast = STAR;
                            break;

                        default:
                            consume();
                            break;
                    }
                    break;

                case MULTI_LINE_COMMENT:
                    switch (ch) {
                        case '*':
                            if (fLast == SLASH_STAR) {
                                fLast = SLASH_STAR_STAR;
                                fTokenLength++;
                                fState = JAVADOC;
                            } else {
                                fTokenLength++;
                                fLast = STAR;
                            }
                            break;

                        case '/':
                            if (fLast == STAR) {
                                return postFix(MULTI_LINE_COMMENT);
                            } else {
                                consume();
                                break;
                            }

                        default:
                            consume();
                            break;
                    }
                    break;

                case STRING:
                    switch (ch) {
                        case '\\':
                            fLast = (fLast == BACKSLASH) ? NONE : BACKSLASH;
                            fTokenLength++;
                            break;

                        case '\"':
                            if (fLast != BACKSLASH) {
                                return postFix(STRING);

                            } else {
                                consume();
                                break;
                            }

                        default:
                            consume();
                            break;
                    }
                    break;

                case CHARACTER:
                    switch (ch) {
                        case '\\':
                            fLast = (fLast == BACKSLASH) ? NONE : BACKSLASH;
                            fTokenLength++;
                            break;

                        case '\'':
                            if (fLast != BACKSLASH) {
                                return postFix(CHARACTER);

                            } else {
                                consume();
                                break;
                            }

                        default:
                            consume();
                            break;
                    }
                    break;
                default:
                    break;
            }
        }
    }

    private static final int getLastLength(int last) {
        switch (last) {
            default:
                return -1;

            case NONE:
                return 0;

            case CARRIAGE_RETURN:
            case BACKSLASH:
            case SLASH:
            case STAR:
                return 1;

            case SLASH_STAR:
                return 2;

            case SLASH_STAR_STAR:
                return 3;
        }
    }

    private final void consume() {
        fTokenLength++;
        fLast = NONE;
    }

    private final Token postFix(int state) {
        fTokenLength++;
        fLast = NONE;
        fState = JAVA;
        fPrefixLength = 0;
        return fTokens[state];
    }

    private final Token preFix(int state, int newState, int last, int prefixLength) {
        // emulate JavaPartitionScanner
        if (fEmulate && state == JAVA && (fTokenLength - getLastLength(fLast) > 0)) {
            fTokenLength -= getLastLength(fLast);
            fJavaOffset = fTokenOffset;
            fJavaLength = fTokenLength;
            fTokenLength = 1;
            fState = newState;
            fPrefixLength = prefixLength;
            fLast = last;
            return fTokens[state];

        } else {
            fTokenLength -= getLastLength(fLast);
            fLast = last;
            fPrefixLength = prefixLength;
            final Token token = fTokens[state];
            fState = newState;
            return token;
        }
    }

    public void setScannedString(String content) {

        fScanner.setScannedString(content);

        fTokenOffset = 0;
        fTokenLength = 0;
        fPrefixLength = 0;
        fLast = NONE;
        fState = JAVA;

        // emulate JavaPartitionScanner
        if (fEmulate) {
            fJavaOffset = -1;
            fJavaLength = 0;
        }
    }

    @Override
    public void setLegalLineDelimiters(final List<String> delimiters) {
        this.fScanner.setLegalLineDelimiters(delimiters);
    }

    /*
     * @see ITokenScanner#getTokenLength()
     */
    public int getTokenLength() {
        return fTokenLength;
    }

    /*
     * @see ITokenScanner#getTokenOffset()
     */
    public int getTokenOffset() {
        return fTokenOffset;
    }

}
